Within this notebook, you'll learn the basics of how to use folium to generate maps with custom layers layers are a visual overlay that can be placed on a map, often based on a map location to provide detailed information about an area

We will also be using the Nominatim REST API through the geopy library to easily access open-source geocoding data based on OpenStreetMap This project can easily be used in any type of environment outside of python through setting up a flask server

When using Nominatim, Please look at the usage policy before using: https://operations.osmfoundation.org/policies/nominatim/

First we will need to import our required libraries. If you haven't already remember to install all dependencies in side of the requirements.txt file [pip i requirements.txt]

In [ ]:
import pandas as pd
import folium
from pprint import pprint
from geopy.geocoders import Nominatim

For our first example we will highlight the location of calgary on a map and adding a marker for the UofC. First we will need to set up our geopy library to have the proper user-agent for the api to authenticate

In [ ]:
geolocator = Nominatim(user_agent="http")

Lets start a new Query where we want to get information on Calgary and more specifically, we want to get the shape of it

In [ ]:
location = geolocator.geocode("Calgary AB Canada", geometry='geoJson')

We can print out the response from the api with .raw

In [ ]:
pprint(location.raw)
{'boundingbox': ['50.842526', '51.2125013', '-114.3157587', '-113.8600018'],
 'class': 'boundary',
 'display_name': 'Calgary, Alberta, Canada',
 'geojson': {'coordinates': [[[-114.3157587, 51.1397612],
                              [-114.3156155, 51.1396239],
                              ....
                              [-114.3041835, 51.1396886],
                              [-114.3157587, 51.1397612]]],
             'type': 'Polygon'},
 'icon': 'https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png',
 'importance': 0.8614801395324394,
 'lat': '51.0460954',
 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. '
            'https://osm.org/copyright',
 'lon': '-114.065465',
 'osm_id': 3227127,
 'osm_type': 'relation',
 'place_id': 282785438,
 'type': 'administrative'}

Here we can see that we get lots of information about the query that we made in json format. We want to get the shape of calgary so we will be looking at geojson > coordinates Here we get a list of a list with the positional coordinates of each polygon point in [[long, lat],... ] pairings

For our mapping tool we will be using folium which takes polygons in with a different format [(lat, long),... ] so lets clean up our data to have that pairing format

If we look again at the result we got from nominatim, we can see that theres another dimension to the geoJson. This is because a query can result in multiple disconnected polygons that will represent the same place. For example: the islands east of BC. Since Calgary Alberta is a single connected location we don't need to worry about that and can just take the first and only element

In [ ]:
geoJson = location.raw['geojson']['coordinates'][0]
# format data from [[],[]...] to [(),(),...]
geoJson = [(x[1],x[0]) for x in geoJson]
pprint(geoJson)
[(51.1397612, -114.3157587),
 (51.1396239, -114.3156155),
 ...
 (51.1396886, -114.3041835),
 (51.1397612, -114.3157587)]

Perfect our data is cleaned up and ready to be used in a map.

To initalize our map, we will call the factory map method inside of folium and store that as a variable. At any time, we can call that variable to display our map inside of a jupyter notebook. [In order to draw an image outside of jupyter, you'll need to save it to a file. This can be done with the .save("<filename.html>") function. You can then open it in a browser to view it]

We will give this factory method the starting location that we want the map to be zoomed into and its initial zoom level

In [ ]:
my_map = folium.Map(location=[51.0486, -114.0708], zoom_start=11)

Lets start drawing our custom layers. We will want a layer to highlight the geometry of calgary and also a seperate layer to display the location of its university.

With folium we can create a Feature Group, which we can add our overlays too.

After making our feature group, we can then bind it to our map.

In order to control/toggle layers we will add a layer controller to the map

In [ ]:
city_layer = folium.FeatureGroup(name="Calgary")
city_layer.add_to(my_map)

university_layer = folium.FeatureGroup(name="University")
university_layer.add_to(my_map)

folium.LayerControl().add_to(my_map)
Out[ ]:
<folium.map.LayerControl at 0x1dc63a86440>

Now we can start drawing on our map! to start, lets draw the geometry for calgary with the polygon tool in folium. We will supply it with the geoJson that we just cleaned, a stroke color, stroke weight, fill colour and opacity. We don't want to draw this directly onto the map, since we want to toggle it, so we will be adding it to our respective feature layer

In [ ]:
# generate polygon for calgary
folium.Polygon(geoJson, 
                color='red', 
                weight=5, 
                fill=True, 
                fill_color='green',
                fill_opacity=0.6).add_to(city_layer)
Out[ ]:
<folium.vector_layers.Polygon at 0x1dc63a87a30>

Next lets add a marker for the university on the map. We will once again be using Nominatim to query for the University of calgary

In [ ]:
# add marker for University of Calgary
location = geolocator.geocode("University Of Calgary")
pprint(location.raw)
folium.Marker(location=[location.latitude, location.longitude],
                popup="<h1>University of Calgary</h1>",
                icon=folium.Icon(color='blue', icon='info-sign')).add_to(university_layer)

folium.CircleMarker(location=[location.latitude, location.longitude],
                    radius=50,
                    popup="<h1>University of Calgary</h1>",
                    color='blue',
                    fill=True,
                    fill_color='blue').add_to(university_layer)
{'boundingbox': ['51.0783151', '51.0784151', '-114.1283572', '-114.1282572'],
 'class': 'amenity',
 'display_name': 'University of Calgary, 500 Campus Place NW, Charleswood, '
                 'Calgary, Alberta, T2N 1N7, Canada',
 'icon': 'https://nominatim.openstreetmap.org/ui/mapicons//education_university.p.20.png',
 'importance': 0.31100000000000005,
 'lat': '51.0783651',
 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. '
            'https://osm.org/copyright',
 'lon': '-114.1283072',
 'osm_id': 29023292,
 'osm_type': 'node',
 'place_id': 111865,
 'type': 'university'}
Out[ ]:
<folium.vector_layers.CircleMarker at 0x1dc63a844f0>
In [ ]:
my_map.save('map.html')
my_map
Out[ ]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [ ]:
Provinces = ["British Columbia", "Alberta", "Saskatchewan", "Manitoba", "Ontario", "Quebec", "New Brunswick", "Nova Scotia", "Prince Edward Island", "Newfoundland and Labrador", "Northwest Territories", "Nunavut", "Yukon"]
Country = "Canada"
   
df = pd.DataFrame(columns=["Location Name", "Latitude", "Longitude", "geometry", "weather"])
for province in Provinces:
    row = {"Location Name": province + ', ' + Country, "Latitude": 0, "Longitude": 0, "geometry": None, "weather": None}
    # add to the end of the dataframe
    df = pd.concat([df, pd.DataFrame(row, index=[len(df)])])


# loop through all provinces on df and append the geometry to the geometries list
for index, row in df.iterrows():
    location = geolocator.geocode(row["Location Name"], geometry='geoJson')
    if location is not None:
        row["Latitude"] = location.latitude
        row["Longitude"] = location.longitude
        geoJson = location.raw['geojson']['coordinates'][0]
        row["geometry"] = geoJson
    
# add random weather data to the df
import random
for index, row in df.iterrows():
    row["weather"] = random.randint(-30,30)


canada_map = folium.Map(location=[51.0486, -114.0708], zoom_start=3)
feature_group_dictionary = {}
for index, row in df.iterrows():
    if row["Location Name"] not in feature_group_dictionary:
        feature_group_dictionary[row["Location Name"]] = folium.FeatureGroup(name=row["Location Name"])
        feature_group_dictionary[row["Location Name"]].add_to(canada_map)

for indew, row in df.iterrows():
    if row["geometry"] is not None:
        geoJson = row["geometry"]
        if type(geoJson[0][0]) == list:
            for island in geoJson:
                island = [(x[1],x[0]) for x in island]
                row["geometry"] = island
                folium.Polygon( island, 
                                color='red', 
                                weight=5, 
                                fill=True, 
                                fill_color='red' if row['weather'] > 0 else 'blue',
                                fill_opacity=abs(row['weather'])/(30 + 20)).add_to(feature_group_dictionary[row["Location Name"]])
        else:
            # format data from [[],[]...] to [(),(),...]
            geoJson = [(x[1],x[0]) for x in geoJson]
            # update the row with the new geometry
            row["geometry"] = geoJson
            folium.Polygon( geoJson, 
                            color='red', 
                            weight=5, 
                            fill=True, 
                            fill_color='red' if row['weather'] > 0 else 'blue',
                            fill_opacity=abs(row['weather'])/(30 + 20)).add_to(feature_group_dictionary[row["Location Name"]])
    # add marker for that province
    popup = folium.Popup(str(row['Location Name']) + "<br>"  + str(row["weather"]) + '&deg;C', max_width=500)
    folium.Marker(location=[row["Latitude"], row["Longitude"]],
                popup=popup,
                icon=folium.Icon(color='blue', icon='info-sign')).add_to(feature_group_dictionary[row["Location Name"]])

folium.LayerControl().add_to(canada_map)
pprint(df)

canada_map
                        Location Name   Latitude   Longitude  \
0            British Columbia, Canada  55.001251 -125.002441   
1                     Alberta, Canada  55.001251 -115.002136   
2                Saskatchewan, Canada  55.532126 -106.141224   
3                    Manitoba, Canada  55.001251  -97.001038   
4                     Ontario, Canada  50.000678  -86.000977   
5                      Quebec, Canada  52.476089  -71.825867   
6               New Brunswick, Canada  46.500283  -66.750183   
7                 Nova Scotia, Canada   45.19604  -63.165379   
8        Prince Edward Island, Canada  46.503545  -63.595517   
9   Newfoundland and Labrador, Canada  53.821733  -61.229553   
10      Northwest Territories, Canada       65.0      -118.0   
11                    Nunavut, Canada  65.037773  -92.554079   
12                      Yukon, Canada  63.000147 -136.002502   

                                             geometry weather  
0   [(59.9996374, -139.0613278), (59.9948369, -139...      10  
1   [(54.9957051, -120.0013835), (54.9254324, -120...       1  
2   [(59.9996139, -110.006368), (59.6484782, -110....      16  
3   [(59.8017646, -102.0075806), (59.7757516, -102...      19  
4   [(52.6236382, -95.1537399), (52.6233596, -95.1...      -5  
5   [(54.63156, -79.7619499), (54.6309, -79.75954)...     -17  
6   [(47.2971796, -69.0534663), (47.296441, -69.05...      -9  
7   [(44.245, -66.6779952), (44.241, -66.677902), ...      17  
8   [(46.6811101, -64.4137999), (46.6809463, -64.4...       2  
9   [(54.0257023, -67.8216853), (54.0234152, -67.8...      -1  
10  [(69.4031067, -136.588001), (68.9891294, -136....      -6  
11  [(68.0002875, -120.6828104), (67.9825881, -120...      24  
12  [(69.7041667, -141.00275), (69.703632, -141.00...     -24  
Out[ ]:
Make this Notebook Trusted to load map: File -> Trust Notebook